/**
 * Class: SongAnalyst
 *
 * Purpose: 
 *
 * Created: Jan 20, 2010 @ 11:42:18 AM
 * @author Ken Scott
 */
package org.openfantasia.songanalyst;

import ddf.minim.AudioBuffer;
import ddf.minim.AudioOutput;
import ddf.minim.AudioPlayer;
import ddf.minim.Minim;
import ddf.minim.analysis.FFT;
import ddf.minim.signals.SineWave;
import org.openfantasia.musica.midi.MidiAnalyst;
import org.openfantasia.songanalyst.controls.PControls;
import org.openfantasia.ui.PColor;
import org.openfantasia.ui.PFrame;
import org.openfantasia.util.TextUtils;
import org.openfantasia.util.math.DRange;
import org.openfantasia.util.math.Math1D;
import org.openfantasia.util.math.Math2D;
import org.openfantasia.util.math.PMath;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

public class SongObserver extends PFrame {

	public static void main(String args[]) {
		PFrame.main(args);

		if (!TextUtils.containsIgnoreCase(args, "--present")) {
			Frame root = Frame.getFrames()[0];
			SongObserver applet = (SongObserver) root.getComponent(0);
			root.remove(applet);
			root.setLayout(new BorderLayout());
			root.add(BorderLayout.NORTH, applet);

			applet.controls = new PControls(applet, 100);
			root.add(BorderLayout.SOUTH, applet.controls);
			root.setSize(root.getWidth(), root.getHeight() + applet.controls.getHeight());
			applet.controls.invalidate();
		}
	}

	// LABELS

	public static final String CURSOR = "cursor";

	public static final String TIME = "time";

	public static final String POWER_CURVE = "power_curve";

	public static final String MIN_FREQUENCY = "min-frequency";

	public static final String MAX_FREQUENCY = "max-frequency";

	public static final String BAND_FREQUENCIES = "band_frequencies";

	public static final String LIVE_POWER = "live_power";

	public static final String LIVE_SCALED = "live_scaled";

	public static final String CYCLE_COUNTER = "cycle_counter";

	public static final String PEAK = "peak";

	public static final String PEAK_AVERAGE = "peak_average";

	public static final String LIVE = "live";

	public static final String LIVE_AVERAGE = "live_average";

	public static final String LIVE_AVERAGE_RMS = "live_average_rms";

	public static final String SMOOTH_PIPE = "smooth_pipe";

	public static final String SMOOTH_AVERAGE = "smooth_average";

	public static final String FULL_AVERAGE = "full_average";

	public static final String TEST_FREQUENCY = "test_frequency";

	public static final String FULL_SMOOTH_AVERAGE = "full_smooth_average";

	public static final String RMS_SMOOTH_AVERAGE = "rms_smooth_average";

	public static final String RMS2_SMOOTH_AVERAGE = "rms2_smooth_average";

	public static final String PLAY = "play";

	public static final String BACK = "back";

	public static final String STOP = "stop";

	public static final String FFWD = "ffwd";

	public static final String FREV = "frev";

	public static final String PAUSE = "pause";

	public static final String RECORD = "record";

	public static final String TO_END = "to-end";

	public static final String TO_START = "to-start";

	public static final float[] STD_OCTAVES = {27.5f, 55f, 110f, 220f, 440f, 880f, 1760f, 3520f, 7540f, 15080f, 30160f};

	public static final String LOW_BASS = "low-bass";		// 10 to 100

	public static final String MID_BASS = "mid-bass";		// 100 to 300

	public static final String LOW_MIDRANGE = "low-midrange";	// 300 to 600

	public static final String MIDRANGE = "midrange";	// 600 to 1200

	public static final String HIGH_MIDRANGE = "high-midrange";		// 1200 to 2400

	public static final String LOWER_HIGHS = "lower-highs";		// 2400 to 4800

	public static final String MIDDLE_HIGHS = "middle-highs";	// 4800 to 9600

	public static final String TOP_END = "top-end";		// 9600 to 20000

	public static final String LOW_BASS_POWER = "low-bass-power";		// 10 to 100

	public static final String MID_BASS_POWER = "mid-bass-power";		// 100 to 300

	public static final String LOW_MIDRANGE_POWER = "low-midrange-power";	// 300 to 600

	public static final String MIDRANGE_POWER = "midrange-power";	// 600 to 1200

	public static final String HIGH_MIDRANGE_POWER = "high-midrange-power";		// 1200 to 2400

	public static final String LOWER_HIGHS_POWER = "lower-highs-power";		// 2400 to 4800

	public static final String MIDDLE_HIGHS_POWER = "middle-highs-power";	// 4800 to 9600

	public static final String TOP_END_POWER = "top-end-power";		// 9600 to 20000

	public static final Map<String, DRange> FREQUENCY_BAND_MAP = new HashMap<String, DRange>();


	static {
		FREQUENCY_BAND_MAP.put(LOW_BASS, new DRange(10d, 100d));					// ultra-low bass, rumble
		FREQUENCY_BAND_MAP.put(MID_BASS, new DRange(100d, 300d));					// bass, vowel base freqs, accompaniment
		FREQUENCY_BAND_MAP.put(LOW_MIDRANGE, new DRange(300d, 600d));			// vowels, voice deep foundation
		FREQUENCY_BAND_MAP.put(MIDRANGE, new DRange(600d, 1200d));				// middle voice, instruments
		FREQUENCY_BAND_MAP.put(HIGH_MIDRANGE, new DRange(1200d, 2400d));	// precision voice, precision instruments
		FREQUENCY_BAND_MAP.put(LOWER_HIGHS, new DRange(2400d, 4800d));		// high-end voice, brass
		FREQUENCY_BAND_MAP.put(MIDDLE_HIGHS, new DRange(4800d, 9600d));		// largely useless
		FREQUENCY_BAND_MAP.put(TOP_END, new DRange(9600d, 20000d));				// largely useless
	}

	// SETTINGS

	float fftSize = pow(2, 12);

	// FIELDS

	boolean showPeaks = false;

	boolean showPeakAvg = false;

	boolean showLive = false;

	boolean showLiveAvg = false;

	boolean showAvg = false;

	SineWave testSignal;

	AudioOutput audioOut;

	boolean testSignalActive = false;

	Minim minim;

	AudioPlayer player;

	FFT fft;

	float[] sharpenKernal1d;

	float[][] timeKernal2d;

	float baseFreq = 55f;

	float minFreq;

	float maxFreq;

	float octaveCount = 7;

	float toneCount = 12;

	float testFreq;

	float vScale = 0.005f;

	int hStep = 4;

	float hScale = 1.0f / (float) hStep;

	float cycleCounter = 0;

	float bufferSize = fftSize * hScale / 2.0f;

	float[] peak = new float[(int) bufferSize];

	float[] avgPower = new float[(int) bufferSize];

	float[] fullAverage = new float[(int) bufferSize];

	LinkedList<Float> peakPipe = new LinkedList<Float>();

	float[] bandFreqs;

	float[] livePower = new float[(int) bufferSize];

	int pipeLength = 15;

	float[][] powerPipe = new float[pipeLength][(int) bufferSize];

	float[][] smoothPipe;

	float[] smoothAverage;

	String audioFile;

	String movieFile;

	String midiFile;

	MidiAnalyst midi;


	public void setup(int width, int height, String movieFile, String renderer, final boolean realtime, double fps, String audioFile, String midiFile, double midiSkew) {
		super.setup();
		size(width, height, renderer);

		this.realtime = realtime;
		this.fps = fps;

		this.audioFile = audioFile;
		this.movieFile = movieFile;
		this.midiFile = midiFile;
		if (this.midiFile != null) {
			this.midi = new MidiAnalyst(this.midiFile);
			this.midi.play(timebase, listeners, midiSkew);
		}

		if (movieFile != null) {
			File file = new File(movieFile);
			try {
				startRecording(file, 30d, 1.0);
			}
			catch (IOException e) {
				e.printStackTrace();
			}
		}

		if (audioFile != null) {
			setupAudio(audioFile);
		}
	}


	public void setupAudio(String audioFile) {
		if (realtime && (audioFile != null)) {
			minim = new Minim(this);

			player = minim.loadFile(audioFile, (int) fftSize);
			audioOut = minim.getLineOut(Minim.STEREO);

			timebase.reset(0, player.length() * 1e6);

			fft = new FFT((int) fftSize, player.sampleRate());
			fft.logAverages((int) baseFreq, (int) toneCount);

			sharpenKernal1d = new float[3];
			sharpenKernal1d[0] = 2.0f;
			sharpenKernal1d[1] = 1.0f;
			sharpenKernal1d[2] = 0.0f;

			timeKernal2d = new float[pipeLength][1];
			for (int i = 0; i < pipeLength; i++) {
				timeKernal2d[i][0] = (float) 1.0 / ((float) i + 1f);
			}
		}
	}


	public void stop() {
		stopEventRecording();
		stopRecording(audioFile, 0);

		if (player != null && (player.isPlaying())) {
			try {
				player.close();
			}
			catch (Throwable t) {
			}
		}
		if (minim != null) {
			minim.stop();
		}

		super.stop();
	}


	public void startEventRecording(String path) {
		try {
			setDrawing(false);
			listeners.startRecording(path);
		}
		catch (IOException e) {
			e.printStackTrace();
		}
	}


	public void stopEventRecording() {
		try {
			listeners.stopRecording();
			setDrawing(true);
		}
		catch (IOException e) {
			e.printStackTrace();
		}
	}


	public void startEventPlayback(String path) {
		try {
			listeners.playback(new File(path));
			realtime = false;
		}
		catch (IOException e) {
			e.printStackTrace();
		}
	}


	public double getFps() {
		return fps;
	}


	public void setFps(double fps) {
		this.fps = fps;
	}


	public String getAudioFile() {
		return audioFile;
	}


	public void setAudioFile(String audioFile) {
		this.audioFile = audioFile;
	}


	public String getMovieFile() {
		return movieFile;
	}


	public void setMovieFile(String movieFile) {
		this.movieFile = movieFile;
	}


	public void setFractionalTime(double frac) {
		super.setFractionalTime(frac);
		if ((timebase != null) && (player != null)) {
			if (player.isPlaying()) {
				player.play((int) timebase.getTimeMS());
			}
			else {
				player.cue((int) timebase.getTimeMS());
			}
		}
	}


	public void toStartAction() {
		timebase.setRunning(false);
		timebase.toStart();
		timebase.setSpeed(1.0);
		if ((player != null) && (player.isPlaying())) {
			player.pause();
			player.mute();
		}
	}


	public void toEndAction() {
		timebase.setRunning(false);
		timebase.toEnd();
		timebase.setSpeed(1.0);
		if ((player != null) && (player.isPlaying())) {
			player.pause();
			player.mute();
		}
	}


	public void playAction() {
		timebase.setSpeed(1.0);
		timebase.setRunning(true);
		if ((player != null) && (((double) player.position() / 1000d) < timebase.getEndTime())) {
			player.cue((int) timebase.getTimeMS());
			player.unmute();
			player.play();
		}
	}


	public void backAction() {
		timebase.setSpeed(-1.0);
		timebase.setRunning(true);
		if ((player != null) && (player.isPlaying())) {
			player.pause();
			player.mute();
		}
	}


	public void stopAction() {
		timebase.setRunning(false);
		if ((player != null) && (player.isPlaying())) {
			player.pause();
			player.mute();
		}
	}


	public void ffwdAction() {
		timebase.setSpeed(10.0);
		timebase.setRunning(true);
		if ((player != null) && (player.isPlaying())) {
			player.pause();
			player.mute();
		}
	}


	public void frevAction() {
		timebase.setSpeed(-10.0);
		timebase.setRunning(true);
		if ((player != null) && (player.isPlaying())) {
			player.pause();
			player.mute();
		}
	}


	public long getEndTimeMS() {
		return timebase.getEndTimeNS() * 1000000;
	}


	public double getEndTimeS() {
		return (double) timebase.getEndTimeNS() / 1e9;
	}


	public void setTimeScale(double scale) {
		timebase.setSpeed(scale);
	}


	public double getTimeScale() {
		return timebase.getSpeed();
	}


	public void draw() {
		broadcast(TIME, getTime());

		if (listeners.isPlayback()) {
			double now = getTime();
			double before = now - (1 / fps);
			listeners.playback(before, now);
		}

		super.draw();

		if (frameCount % 1000 == 0) {
			System.out.println("Time: " + getTime());
			System.out.println("Rate: " + frameRate);
		}
	}


	public void preDraw() {
		if ((player != null) && player.isPlaying()) {
			AudioBuffer buffer = player.mix;

			// Do the FFT on the audio stream
			fft.forward(buffer);

			// Record the band frequencies
			if (bandFreqs == null) {
				bandFreqs = new float[(int) bufferSize];
				for (int i = 0; i < (int) bandFreqs.length; i++) {
					bandFreqs[i] = fft.indexToFreq(i);
				}
				minFreq = bandFreqs[0];
				maxFreq = bandFreqs[bandFreqs.length - 1];
			}

			// Grab the current power levels
			for (int i = 0; i < (int) bufferSize; i++) {
				livePower[i] = fft.getBand(i);
			}
			float[] liveScaled = Math1D.scale(vScale, Math1D.mul(livePower, bandFreqs));

			// Push the power levels into a smoothing pipeline
			for (int i = pipeLength - 1; i > 0; i--) {
				System.arraycopy(powerPipe[i - 1], 0, powerPipe[i], 0, (int) bufferSize);
			}
			System.arraycopy(livePower, 0, powerPipe[0], 0, (int) bufferSize);
			smoothPipe = Math2D.filter(powerPipe, timeKernal2d);
			smoothAverage = PMath.mix(smoothPipe);
			float avg = Math1D.avg(smoothAverage);

			float rms2 = PMath.sqrt(2f) * avg;
			float rms = rms2 / 2.0f;

			// Find the peaks
			for (int i = 0; i < (int) bufferSize; i++) {
				if (livePower[i] > peak[i]) {
					peak[i] = livePower[i];
				}
			}

			// Keep a running average
			if (getTime() < getEndTimeS()) {
				cycleCounter += 1.0;
				for (int i = 0; i < (int) bufferSize; i++) {
					avgPower[i] = ((avgPower[i] * (cycleCounter - 1)) + livePower[i]) / cycleCounter;
				}
				fullAverage = Math1D.filter(avgPower, sharpenKernal1d);
			}

			// Update the test signal
			float dx = (float) mouseX / (float) width;
			float df = maxFreq - minFreq;
			testFreq = (dx * df) + minFreq;
			synchronized (this) {
				if ((testSignal != null) && (testFreq != testSignal.frequency())) {
					testSignal.setFreq(testFreq);
				}
			}

			// Let the listeners know that something(s) happened

			broadcast(CYCLE_COUNTER, cycleCounter);

			// frequency spectrum
			broadcast(MIN_FREQUENCY, minFreq);
			broadcast(MAX_FREQUENCY, maxFreq);
			broadcast(BAND_FREQUENCIES, bandFreqs);

			// scalars
			broadcast(LIVE_AVERAGE, avg);
			broadcast(LIVE_AVERAGE_RMS, rms);
			broadcast(LIVE_SCALED, liveScaled);

			// arrays
			broadcast(SMOOTH_AVERAGE, smoothAverage);
			broadcast(LIVE_POWER, livePower);
			broadcast(PEAK, peak);
			broadcast(FULL_AVERAGE, fullAverage);

			// band power
			if (listeners.hasListeners(LOW_BASS_POWER)) {
				broadcast(LOW_BASS_POWER, bandAverage(FREQUENCY_BAND_MAP.get(LOW_BASS), livePower));
			}
			if (listeners.hasListeners(MID_BASS_POWER)) {
				broadcast(MID_BASS_POWER, bandAverage(FREQUENCY_BAND_MAP.get(MID_BASS), livePower));
			}
			if (listeners.hasListeners(LOW_MIDRANGE_POWER)) {
				broadcast(LOW_MIDRANGE_POWER, bandAverage(FREQUENCY_BAND_MAP.get(LOW_MIDRANGE), livePower));
			}
			if (listeners.hasListeners(MIDRANGE_POWER)) {
				broadcast(MIDRANGE_POWER, bandAverage(FREQUENCY_BAND_MAP.get(MIDRANGE), livePower));
			}
			if (listeners.hasListeners(HIGH_MIDRANGE_POWER)) {
				broadcast(HIGH_MIDRANGE_POWER, bandAverage(FREQUENCY_BAND_MAP.get(HIGH_MIDRANGE), livePower));
			}
			if (listeners.hasListeners(LOWER_HIGHS_POWER)) {
				broadcast(LOWER_HIGHS_POWER, bandAverage(FREQUENCY_BAND_MAP.get(LOWER_HIGHS), livePower));
			}
			if (listeners.hasListeners(MIDDLE_HIGHS_POWER)) {
				broadcast(MIDDLE_HIGHS_POWER, bandAverage(FREQUENCY_BAND_MAP.get(MIDDLE_HIGHS), livePower));
			}
			if (listeners.hasListeners(TOP_END_POWER)) {
				broadcast(TOP_END_POWER, bandAverage(FREQUENCY_BAND_MAP.get(TOP_END), livePower));
			}

			// bands
			if (listeners.hasListeners(LOW_BASS)) {
				broadcast(LOW_BASS, bandPass(FREQUENCY_BAND_MAP.get(LOW_BASS), livePower));
			}
			if (listeners.hasListeners(MID_BASS)) {
				broadcast(MID_BASS, bandPass(FREQUENCY_BAND_MAP.get(MID_BASS), livePower));
			}
			if (listeners.hasListeners(LOW_MIDRANGE)) {
				broadcast(LOW_MIDRANGE, bandPass(FREQUENCY_BAND_MAP.get(LOW_MIDRANGE), livePower));
			}
			if (listeners.hasListeners(MIDRANGE)) {
				broadcast(MIDRANGE, bandPass(FREQUENCY_BAND_MAP.get(MIDRANGE), livePower));
			}
			if (listeners.hasListeners(HIGH_MIDRANGE)) {
				broadcast(HIGH_MIDRANGE, bandPass(FREQUENCY_BAND_MAP.get(HIGH_MIDRANGE), livePower));
			}
			if (listeners.hasListeners(LOWER_HIGHS)) {
				broadcast(LOWER_HIGHS, bandPass(FREQUENCY_BAND_MAP.get(LOWER_HIGHS), livePower));
			}
			if (listeners.hasListeners(MIDDLE_HIGHS)) {
				broadcast(MIDDLE_HIGHS, bandPass(FREQUENCY_BAND_MAP.get(MIDDLE_HIGHS), livePower));
			}
			if (listeners.hasListeners(TOP_END)) {
				broadcast(TOP_END, bandPass(FREQUENCY_BAND_MAP.get(TOP_END), livePower));
			}
		}
	}


	public float bandAverage(DRange dRange, float[] power) {
		float sum = 0;
		float count = 0;
		for (int i = 0; i < power.length; i++) {
			boolean in = dRange.isWithin(bandFreqs[i]);
			sum += in ? power[i] : 0;
			count += in ? 1 : 0;
		}
		return sum / count;
	}


	public float[] bandPass(DRange dRange, float[] power) {
		float[] result = Math1D.copy(power);
		for (int i = 0; i < result.length; i++) {
			result[i] = dRange.isWithin(bandFreqs[i]) ? result[i] : 0;
		}
		return result;
	}


	public void setStyle() {
		noFill();
		ellipseMode(CENTER);
		rectMode(CORNER);
		stroke(PColor.WHITE);
	}


	public void setLighting() {
		directionalLight(.5f, .5f, .5f, 1, 1, -1);
		ambientLight(.25f, .25f, .25f);
		specular(.5);
	}


	public void setCamera() {
		camera(0, 0, Math.max(width, height), 0, 0, 0, 0f, -1f, 0f);
	}
}